package com.duckclouddrive.service.impl;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;

import com.duckclouddrive.component.RedisComponent;
import com.duckclouddrive.entity.config.AppConfig;
import com.duckclouddrive.entity.constants.Constants;
import com.duckclouddrive.entity.dto.DownloadFileDto;
import com.duckclouddrive.entity.dto.SessionWebUserDto;
import com.duckclouddrive.entity.dto.UploadResultDto;
import com.duckclouddrive.entity.dto.UserSpaceDto;
import com.duckclouddrive.entity.enums.*;
import com.duckclouddrive.entity.po.UserInfo;
import com.duckclouddrive.entity.query.UserInfoQuery;
import com.duckclouddrive.exception.BusinessException;
import com.duckclouddrive.mappers.UserInfoMapper;
import com.duckclouddrive.utils.DateUtil;
import com.duckclouddrive.utils.ProcessUtils;
import com.duckclouddrive.utils.ScaleFilter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.aspectj.util.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import com.duckclouddrive.entity.query.FileInfoQuery;
import com.duckclouddrive.entity.po.FileInfo;
import com.duckclouddrive.entity.vo.PaginationResultVO;
import com.duckclouddrive.entity.query.SimplePage;
import com.duckclouddrive.mappers.FileInfoMapper;
import com.duckclouddrive.service.FileInfoService;
import com.duckclouddrive.utils.StringTools;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;


/**
 * 文件信息 业务接口实现
 */
@Service("fileInfoService")
public class FileInfoServiceImpl implements FileInfoService {

    private static final Logger logger = LoggerFactory.getLogger(FileInfoServiceImpl.class);
    @Resource
    private FileInfoMapper<FileInfo, FileInfoQuery> fileInfoMapper;

    @Resource
    private RedisComponent redisComponent;

    @Resource
    private UserInfoMapper<UserInfo, UserInfoQuery> userInfoMapper;

    @Resource
    private AppConfig appConfig;
    @Resource
    @Lazy
    private FileInfoServiceImpl fileInfoService;

    /**
     * 根据条件查询列表
     */
    @Override
    public List<FileInfo> findListByParam(FileInfoQuery param) {
        return this.fileInfoMapper.selectList(param);
    }

    /**
     * 根据条件查询列表
     */
    @Override
    public Integer findCountByParam(FileInfoQuery param) {
        return this.fileInfoMapper.selectCount(param);
    }

    /**
     * 根据分页参数查询文件列表
     *
     * @param param 文件查询参数对象，包含分页信息和查询条件
     * @return 返回一个分页结果对象，包含文件列表和分页信息
     */
    @Override
    public PaginationResultVO<FileInfo> findListByPage(FileInfoQuery param) {
        // 计算总记录数
        int count = this.findCountByParam(param);

        // 设置默认页面大小，如果未指定则使用默认大小
        int pageSize = param.getPageSize() == null ? PageSize.SIZE15.getSize() : param.getPageSize();

        // 创建分页对象
        SimplePage page = new SimplePage(param.getPageNo(), count, pageSize);
        // 将分页对象设置到查询参数中
        param.setSimplePage(page);

        // 根据查询参数获取文件列表
        List<FileInfo> list = this.findListByParam(param);

        // 创建并返回分页结果对象
        PaginationResultVO<FileInfo> result = new PaginationResultVO(count, page.getPageSize(), page.getPageNo(), page.getPageTotal(), list);
        return result;
    }

    /**
     * 新增
     */
    @Override
    public Integer add(FileInfo bean) {
        return this.fileInfoMapper.insert(bean);
    }

    /**
     * 批量新增
     */
    @Override
    public Integer addBatch(List<FileInfo> listBean) {
        if (listBean == null || listBean.isEmpty()) {
            return 0;
        }
        return this.fileInfoMapper.insertBatch(listBean);
    }

    /**
     * 批量新增或者修改
     */
    @Override
    public Integer addOrUpdateBatch(List<FileInfo> listBean) {
        if (listBean == null || listBean.isEmpty()) {
            return 0;
        }
        return this.fileInfoMapper.insertOrUpdateBatch(listBean);
    }

    /**
     * 多条件更新
     */
    @Override
    public Integer updateByParam(FileInfo bean, FileInfoQuery param) {
        StringTools.checkParam(param);
        return this.fileInfoMapper.updateByParam(bean, param);
    }

    /**
     * 多条件删除
     */
    @Override
    public Integer deleteByParam(FileInfoQuery param) {
        StringTools.checkParam(param);
        return this.fileInfoMapper.deleteByParam(param);
    }

    /**
     * 根据FileIdAndUserId获取对象
     */
    @Override
    public FileInfo getFileInfoByFileIdAndUserId(String fileId, String userId) {
        return this.fileInfoMapper.selectByFileIdAndUserId(fileId, userId);
    }

    /**
     * 根据FileIdAndUserId修改
     */
    @Override
    public Integer updateFileInfoByFileIdAndUserId(FileInfo bean, String fileId, String userId) {
        return this.fileInfoMapper.updateByFileIdAndUserId(bean, fileId, userId);
    }

    /**
     * 根据FileIdAndUserId删除
     */
    @Override
    public Integer deleteFileInfoByFileIdAndUserId(String fileId, String userId) {
        return this.fileInfoMapper.deleteByFileIdAndUserId(fileId, userId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public UploadResultDto uploadFile(SessionWebUserDto webUserDto, String fileId, MultipartFile file, String fileName, String filePid, String fileMd5,
                                      Integer chunkIndex, Integer chunks) {
        // 创建上传结果对象
        UploadResultDto resultDto = new UploadResultDto();
        Boolean uploadSuccess = true;
        File tempFileFolder = null;
        try {
            // 检查文件ID是否为空，如果为空则生成随机ID
            if (StringTools.isEmpty(fileId)) {
                fileId = StringTools.getRandomString(Constants.LENGTH_10);
            }

            // 设置文件ID到结果对象
            resultDto.setFileId(fileId);
            // 获取当前日期
            Date curDate = new Date();
            // 从Redis中获取用户的空间使用情况
            UserSpaceDto spaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());

            // 如果是文件分块上传的第一个块
            if (chunkIndex == 0) {
                // 创建文件信息查询对象
                FileInfoQuery infoQuery = new FileInfoQuery();
                // 根据文件MD5值查询文件信息
                infoQuery.setFileId(fileMd5);
                // 设置分页参数，只获取第一条记录
                infoQuery.setSimplePage(new SimplePage(0, 1));
                // 设置文件状态为正在使用
                infoQuery.setStatus(FileStatusEnums.USING.getStatus());
                List<FileInfo> dbFileList = this.fileInfoMapper.selectList(infoQuery);
                if (!dbFileList.isEmpty()) { //数据库中有该文件，秒传
                    FileInfo dbFile = dbFileList.get(0);
                    if ((dbFile.getFileSize() + spaceDto.getUseSpace()) > spaceDto.getTotalSpace()) {
                        throw new BusinessException(ResponseCodeEnum.CODE_904);
                    }
                    // 设置文件的唯一标识符
                    dbFile.setFileId(fileId);
                    // 设置文件的父标识符，用于关联父文件
                    dbFile.setFilePid(filePid);
                    // 设置上传文件的用户ID
                    dbFile.setUserId(webUserDto.getUserId());
                    // 设置文件的MD5校验码，此处先设置为null，可能后续需要计算或获取
                    dbFile.setFileMd5(null);
                    // 设置文件的创建时间
                    dbFile.setCreateTime(curDate);
                    // 设置文件的最后更新时间
                    dbFile.setLastUpdateTime(curDate);
                    // 设置文件的状态为“使用中”
                    dbFile.setStatus(FileStatusEnums.USING.getStatus());
                    // 设置文件的删除标志为“使用中”，表示文件未被逻辑删除
                    dbFile.setDelFlag(FileDelFlagEnums.USING.getFlag());
                    // 正式设置文件的MD5校验码
                    dbFile.setFileMd5(fileMd5);
                    // 自动重命名文件以避免重复，并更新文件名
                    fileName = autoRename(filePid, webUserDto.getUserId(), fileName);

                    // 设置新的文件名
                    dbFile.setFileName(fileName);

                    // 插入文件信息到数据库
                    this.fileInfoMapper.insert(dbFile);

                    // 设置上传状态为秒传成功
                    resultDto.setStatus(UploadStatusEnums.UPLOAD_SECONDS.getCode());

                    // 更新用户空间使用情况
                    updateUserSpace(webUserDto, dbFile.getFileSize());

                    // 返回结果DTO
                    return resultDto;
                }
            }
            //判断磁盘空间
            Long currentTempSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
            if (file.getSize() + currentTempSize + spaceDto.getUseSpace() > spaceDto.getTotalSpace()) {
                throw new BusinessException(ResponseCodeEnum.CODE_904);
            }
            // 根据项目配置和常量构造临时文件夹路径
            String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;
            // tempFolderName = appConfig.getProjectFolder()+ Constants.FILE_FOLDER_TEMP;

            // 使用当前用户ID和文件ID构造用户特定的临时文件夹名称
            String currentUserFolder = webUserDto.getUserId() + fileId;
            // 创建用户特定的临时文件夹路径对象
            tempFileFolder = new File(tempFolderName + currentUserFolder);
            // 如果该临时文件夹不存在，则创建它
            if (!tempFileFolder.exists()) {
                tempFileFolder.mkdirs();
            }
            File newFile = new File(tempFileFolder.getPath() + "/" + chunkIndex);
            // 将上传的文件转移到构造的文件路径
            file.transferTo(newFile);
            // 保存临时文件的大小到Redis
            redisComponent.saveFileTempSize(webUserDto.getUserId(), fileId, file.getSize());
            // 如果当前分片不是最后一个分片
            if (chunkIndex < chunks - 1) {
                // 设置结果DTO的状态为正在上传
                resultDto.setStatus(UploadStatusEnums.UPLOADING.getCode());
                // 返回结果DTO，此时上传还在进行中
                return resultDto;
            }
            //最后一个分片上传完成，记录数据库，异步合并分片
            String month = DateUtil.format(new Date(), DateTimePatternEnum.YYYYMM.getPattern());
            String fileSuffix = StringTools.getFileSuffix(fileName);
            //真实文件名
            String realFileName = currentUserFolder + fileSuffix;
            FileTypeEnums fileTypeEnum = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
            //自动重命名
            fileName = autoRename(filePid, webUserDto.getUserId(), fileName);
            FileInfo fileInfo = new FileInfo();
            fileInfo.setFileId(fileId);
            fileInfo.setUserId(webUserDto.getUserId());
            fileInfo.setFileMd5(fileMd5);
            fileInfo.setFileName(fileName);
            fileInfo.setFilePath(month + "/" + realFileName);
            fileInfo.setFilePid(filePid);
            fileInfo.setCreateTime(curDate);
            fileInfo.setLastUpdateTime(curDate);
            fileInfo.setFileCategory(fileTypeEnum.getCategory().getCategory());
            fileInfo.setFileType(fileTypeEnum.getType());
            fileInfo.setStatus(FileStatusEnums.TRANSFER.getStatus());
            fileInfo.setFolderType(FileFolderTypeEnums.FILE.getType());
            fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
            this.fileInfoMapper.insert(fileInfo);
            Long totalSize = redisComponent.getFileTempSize(webUserDto.getUserId(), fileId);
            updateUserSpace(webUserDto, totalSize);
            resultDto.setStatus(UploadStatusEnums.UPLOAD_FINISH.getCode());

            TransactionSynchronizationManager.registerSynchronization(
                    new TransactionSynchronization() {
                        @Override
                        public void afterCommit() {

                            fileInfoService.transferFile(fileInfo.getFileId(), webUserDto);
                        }
                    });

            return resultDto;
        } catch (BusinessException e) {
            logger.error("文件上传失败");
            uploadSuccess = false;
            throw e;
        } catch (Exception e) {
            logger.error("文件上传失败");
            uploadSuccess = false;
        } finally {
            if (!uploadSuccess && tempFileFolder != null) {
                try {
                    FileUtils.deleteDirectory(tempFileFolder);
                } catch (IOException e) {
                    logger.error("删除临时文件失败", e);
                }
            }
        }
        return resultDto;
    }

    private String autoRename(String filePid, String userId, String fileName) {
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setUserId(userId);
        fileInfoQuery.setFilePid(filePid);
        fileInfoQuery.setDelFlag(FileDelFlagEnums.USING.getFlag());
        fileInfoQuery.setFileName(fileName);
        Integer count = this.fileInfoMapper.selectCount(fileInfoQuery);
        if (count > 0) {
            return StringTools.rename(fileName);
        }
        return fileName;
    }

    @Async
    public void transferFile(String fileId, SessionWebUserDto webUserDto) {
        // 初始化一个布尔变量，用于标记文件转移是否成功，默认为false
        Boolean transferSuccess = true;

        // 初始化目标文件路径变量，目前为null，待后续赋值
        String targetFilePath = null;

        // 初始化封面图片变量，目前为null，待后续赋值
        String cover = null;

        // 初始化文件类型枚举变量，用于存储文件的类型，目前为null，待后续赋值
        FileTypeEnums fileTypeEnums = null;

        // 通过文件ID和用户ID查询文件信息
        // 调用fileInfoMapper的selectByFileIdAndUserId方法，传入文件ID和当前用户ID，获取FileInfo对象
        FileInfo fileInfo = this.fileInfoMapper.selectByFileIdAndUserId(fileId, webUserDto.getUserId());
        try {
            // 检查文件信息是否为空或状态不是传输中
            if (fileInfo == null || !FileStatusEnums.TRANSFER.getStatus().equals(fileInfo.getStatus())) {
                return;
            }
            // 根据项目目录和临时文件夹常量构造临时文件夹路径
            String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_TEMP;

            // 使用当前用户ID和文件ID构造用户特定的文件夹名称
            String currentUserFolderName = webUserDto.getUserId() + fileId;

            // 构造一个File对象，用于表示和操作临时目录下的用户特定文件夹
            File fileFolder = new File(tempFolderName + currentUserFolderName);
            String fileSuffix = StringTools.getFileSuffix(fileInfo.getFileName());
            String month = DateUtil.format(fileInfo.getCreateTime(), DateTimePatternEnum.YYYYMM.getPattern());
            //目标目录
            String targetFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER_FILE;
            File targetFolder = new File(targetFolderName + "/" + month);
            if (!targetFolder.mkdir()) {
                targetFolder.mkdirs();
            }
            //真实文件名
            String realFileName = currentUserFolderName + fileSuffix;
            targetFilePath = targetFolder.getPath() + "/" + realFileName;
            //合并文件
            union(fileFolder.getPath(), targetFilePath, fileInfo.getFileName(), true);
            // 视频文件切割
            // 根据文件后缀名获取文件类型枚举
            fileTypeEnums = FileTypeEnums.getFileTypeBySuffix(fileSuffix);
            // 如果文件类型为视频
            if (FileTypeEnums.VIDEO == fileTypeEnums) {
                // 对视频文件进行切割处理
                cutFile4Video(fileId, targetFilePath);
                // 拼接视频封面的路径
                cover = month + "/" + currentUserFolderName + Constants.IMAGE_PNG_SUFFIX;
                String coverPath = targetFolderName + "/" + cover;
                //  targetFilePath =  appConfig.getProjectFolder()+ Constants.FILE_FOLDER_FILE+"/"+month + "/" + currentUserFolderName+fileSuffix;
                //  coverPath = appConfig.getProjectFolder()+ Constants.FILE_FOLDER_FILE+"/"+month + "/" + currentUserFolderName+Constants.IMAGE_PNG_SUFFIX;
                // 创建视频封面
                ScaleFilter.createCover4Video(new File(targetFilePath), Constants.LENGTH_150, new File(coverPath));
            } else if (FileTypeEnums.IMAGE == fileTypeEnums) {
                // 如果文件类型为图片，拼接图片缩略图的路径
                cover = month + "/" + realFileName.replace(".", "_.");
                String coverPath = targetFolderName + "/" + cover;
                // 尝试创建图片缩略图
                Boolean created = ScaleFilter.createThumbnailWidthFFmpeg(new File(targetFilePath), Constants.LENGTH_150, new File(coverPath), false);
                // 如果未能创建缩略图，则直接复制原图作为封面
                if (!created) {
                    FileUtils.copyFile(new File(targetFilePath), new File(coverPath));
                }
            }
        } catch (Exception e) {
            logger.error("文件转码失败，文件Id:{},userId:{}", fileId, webUserDto.getUserId(), e);
            transferSuccess = false;
        } finally {
            // 创建一个FileInfo对象，用于存储文件的更新信息
            FileInfo updateInfo = new FileInfo();
            // 设置文件大小，通过获取目标文件路径下的文件长度来实现
            updateInfo.setFileSize(new File(targetFilePath).length());
            // 设置文件封面，这里的cover变量代表文件的封面信息
            updateInfo.setFileCover(cover);
            // 根据文件传输结果设置文件状态：如果传输成功，则设置为“使用中”；否则，设置为“传输失败”
            updateInfo.setStatus(transferSuccess ? FileStatusEnums.USING.getStatus() : FileStatusEnums.TRANSFER_FAIL.getStatus());
            // 调用DAO方法更新文件状态，根据文件ID、用户ID、更新信息以及文件的旧状态进行更新
            fileInfoMapper.updateFileStatusWithOldStatus(fileId, webUserDto.getUserId(), updateInfo, FileStatusEnums.TRANSFER.getStatus());
        }
    }

    /**
     * 合并分片文件
     *
     * @param dirPath    分片文件所在的目录路径
     * @param toFilePath 合并后文件的目标路径
     * @param fileName   待合并的文件名，用于错误信息提示
     * @param delSource  是否删除合并后的分片文件目录
     * @throws BusinessException 当目录不存在或合并文件过程中发生错误时抛出业务异常
     */
    private void union(String dirPath, String toFilePath, String fileName, boolean delSource) {
        // 检查分片文件目录是否存在
        File dir = new File(dirPath);
        if (!dir.exists()) {
            throw new BusinessException("目录不存在");
        }

        // 获取目录下所有分片文件
        File[] fileList = dir.listFiles();
        // 创建目标文件对象
        File targetFile = new File(toFilePath);
        // 用于文件写入操作
        RandomAccessFile writeFile = null;
        try {
            writeFile = new RandomAccessFile(targetFile, "rw");
            // 缓冲区，用于读写文件
            byte[] b = new byte[1024 * 10];
            // 遍历所有分片文件并合并
            for (int i = 0; i < fileList.length; i++) {
                int len = -1;
                // 每个分片文件的完整路径
                File chunkFile = new File(dirPath + "/" + i);
                RandomAccessFile readFile = null;
                try {
                    readFile = new RandomAccessFile(chunkFile, "r");
                    // 读取分片文件内容并写入目标文件
                    while ((len = readFile.read(b)) != -1) {
                        writeFile.write(b, 0, len);
                    }
                } catch (Exception e) {
                    logger.error("合并分片失败", e);
                    throw new BusinessException("合并分片失败");
                } finally {
                    if (readFile != null) {
                        readFile.close();
                    }
                }
            }
        } catch (Exception e) {
            logger.error("合并文件:{}失败", fileName, e);
            throw new BusinessException("合并文件" + fileName + "出错了");
        } finally {
            try {
                if (null != writeFile) {
                    writeFile.close();
                }
            } catch (IOException e) {
                logger.error("关闭流失败", e);
            }
            // 根据参数决定是否删除源目录
            if (delSource && dir.exists()) {
                try {
                    FileUtils.deleteDirectory(dir);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 对视频文件进行切割，生成多个TS分片文件
     * 此方法主要用于处理视频文件，将其转换为TS格式并按时间分割成多个TS文件
     * 如果视频编码为hevc，则先将其转码为h264编码，然后再进行切割
     *
     * @param fileId        视频文件的唯一标识符
     * @param videoFilePath 视频文件的路径
     */
    private void cutFile4Video(String fileId, String videoFilePath) {
        // 创建存放TS文件的文件夹
        File tsFolder = new File(videoFilePath.substring(0, videoFilePath.lastIndexOf(".")));
        if (!tsFolder.exists()) {
            tsFolder.mkdirs();
        }

        // 使用ffprobe工具获取视频编码信息
        final String CMD_GET_CODE = "ffprobe -v error -select_streams v:0 -show_entries stream=codec_name %s";
        String cmd = String.format(CMD_GET_CODE, videoFilePath);
        String result = ProcessUtils.executeCommand(cmd, false);
        result = result.replace("\n", "");
        result = result.substring(result.indexOf("=") + 1);
        String codec = result.substring(0, result.indexOf("["));

        // 如果视频编码为hevc，先转码为h264
        if ("hevc".equals(codec)) {
            // 为视频文件添加一个下划线后缀，以备份原始文件
            String newFileName = videoFilePath.substring(0, videoFilePath.lastIndexOf(".")) + "_" + videoFilePath.substring(videoFilePath.lastIndexOf("."));
            // 重命名视频文件，进行备份
            new File(videoFilePath).renameTo(new File(newFileName));

            // 定义一个格式转换的命令模板，将视频从HEVC编码转换为H.264编码
            String CMD_HEVC_264 = "ffmpeg -i %s -c:v libx264 -crf 20 %s";
            // 格式化命令，使用备份文件作为输入，转换后覆盖原始视频文件
            cmd = String.format(CMD_HEVC_264, newFileName, videoFilePath);
            // 执行命令，进行视频编码转换
            ProcessUtils.executeCommand(cmd, false);

            // 删除备份文件，清理临时数据
            new File(newFileName).delete();
        }

        // 将视频转换为TS格式
        final String CMD_TRANSFER_2TS = "ffmpeg -y -i %s  -vcodec copy -acodec copy -vbsf h264_mp4toannexb %s";
        final String CMD_CUT_TS = "ffmpeg -i %s -c copy -map 0 -f segment -segment_list %s -segment_time 30 %s/%s_%%4d.ts";
        String tsPath = tsFolder + "/" + Constants.TS_NAME;
        cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
        ProcessUtils.executeCommand(cmd, false);

        // 按照时间段将视频切割成多个TS文件
        cmd = String.format(CMD_CUT_TS, tsPath, tsFolder.getPath() + "/" + Constants.M3U8_NAME, tsFolder.getPath(), fileId);
        ProcessUtils.executeCommand(cmd, false);

        // 删除临时TS文件
        new File(tsPath).delete();
    }

    /**
     * 新建文件夹
     *
     * @param filePid    父文件夹的ID
     * @param userId     用户ID
     * @param folderName 文件夹名称
     * @return 返回新建的文件夹信息
     * @throws BusinessException 如果文件夹已存在，则抛出业务异常
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileInfo newFolder(String filePid, String userId, String folderName) {
        // 检查文件名是否合法，以及用户是否有权限在指定的文件夹下创建文件夹
        checkFileName(filePid, userId, folderName, FileFolderTypeEnums.FOLDER.getType());

        // 获取当前日期，用于记录文件夹的创建和最后修改时间
        Date curDate = new Date();

        // 创建新的文件夹信息对象，并设置各项属性
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileId(StringTools.getRandomString(Constants.LENGTH_10)); // 生成随机的文件夹ID
        fileInfo.setUserId(userId); // 设置用户ID
        fileInfo.setFilePid(filePid); // 设置父文件夹ID
        fileInfo.setFileName(folderName); // 设置文件夹名称
        fileInfo.setFolderType(FileFolderTypeEnums.FOLDER.getType()); // 设置文件夹类型
        fileInfo.setCreateTime(curDate); // 设置创建时间
        fileInfo.setLastUpdateTime(curDate); // 设置最后修改时间
        fileInfo.setStatus(FileStatusEnums.USING.getStatus()); // 设置文件夹状态为使用中
        fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag()); // 设置删除标志为未删除

        // 插入文件夹信息到数据库
        this.fileInfoMapper.insert(fileInfo);

        // 查询条件对象，用于检查同名文件夹是否已存在
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setFilePid(filePid); // 设置父文件夹ID
        fileInfoQuery.setUserId(userId); // 设置用户ID
        fileInfoQuery.setFileName(folderName); // 设置文件夹名称
        fileInfoQuery.setFolderType(FileFolderTypeEnums.FOLDER.getType()); // 设置文件夹类型
        fileInfoQuery.setDelFlag(FileDelFlagEnums.USING.getFlag()); // 设置删除标志为未删除

        // 执行查询并获取结果数量
        Integer count = this.fileInfoMapper.selectCount(fileInfoQuery);
        if (count > 1) {
            // 如果同名文件夹已存在，则抛出异常
            throw new BusinessException("文件夹" + folderName + "已经存在");
        }

        // 返回新建的文件夹信息
        fileInfo.setFileName(folderName); // 确保文件夹名称正确
        fileInfo.setLastUpdateTime(curDate); // 更新最后修改时间
        return fileInfo;
    }

    @Override
    public FileInfo rename(String filename, String fileId, String userId) {
        //查文件中要修改的文件是否存在
        FileInfo fileInfo = this.fileInfoMapper.selectByFileIdAndUserId(fileId, userId);

        if (fileInfo == null) {
            throw new BusinessException("文件不存在");
        }
        String filePid = fileInfo.getFilePid();
        checkFileName(filePid, userId, filename, fileInfo.getFolderType());
        if (FileFolderTypeEnums.FILE.getType().equals(fileInfo.getFolderType())) {
            filename = filename + StringTools.getFileSuffix(fileInfo.getFileName());
        }
        //更新数据库
        Date curDate = new Date();
        FileInfo dbInfo = new FileInfo();
        dbInfo.setFileName(filename);
        dbInfo.setLastUpdateTime(curDate);
        this.fileInfoMapper.updateByFileIdAndUserId(dbInfo, fileId, userId);

        //构造查询条件
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setFileName(filename);
        fileInfoQuery.setFilePid(filePid);
        fileInfoQuery.setUserId(userId);
        //设置文件状态为使用中
        fileInfoQuery.setStatus(FileStatusEnums.USING.getStatus());
        Integer count = this.fileInfoMapper.selectCount(fileInfoQuery);
        if (count > 1) {
            throw new BusinessException("文件名重复");
        }
        //查是否有同名文件夹
        fileInfo.setFileName(filename);
        //更新最新的更新时间
        fileInfo.setLastUpdateTime(curDate);
        return fileInfo;
    }

    @Transactional(rollbackFor = Exception.class)
    public void changeFileFolder(String fileIds, String filePid, String userId) {

        //已经在这个目录了，还往这个目录移动
        if (fileIds.equals(filePid)) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }

        // 如果文件ID不为零值字符串
        if (!Constants.ZERO_STR.equals(filePid)) {
            // 根据文件ID和用户ID获取文件信息
            FileInfo fileInfo = fileInfoService.getFileInfoByFileIdAndUserId(filePid, userId);
            // 如果文件信息不存在或文件处于非使用状态（即已被删除或标记为删除）
            if (fileInfo == null || !FileDelFlagEnums.USING.getFlag().equals(fileInfo.getDelFlag())) {
                // 抛出业务异常，表示文件不存在或已被删除
                throw new BusinessException(ResponseCodeEnum.CODE_600);
            }
        }

        String[] fileIdArray = fileIds.split(",");
        FileInfoQuery query = new FileInfoQuery();
        query.setFilePid(filePid);
        query.setUserId(userId);
        List<FileInfo> dbFileList = fileInfoService.findListByParam(query);

        Map<String, FileInfo> dbFileNameMap = dbFileList.stream().collect(Collectors.toMap(FileInfo::getFileName, Function.identity(), (file1, file2) -> file2));


        //查询选中的文件
        query = new FileInfoQuery();
        query.setUserId(userId);
        query.setFileIdArray(fileIdArray);
        List<FileInfo> selectFileList = fileInfoService.findListByParam(query);

        //将所选文件重命名
        for (FileInfo item : selectFileList) {
            FileInfo rootFileInfo = dbFileNameMap.get(item.getFileName());
            //文件名已经存在，重命名被还原的文件名
            FileInfo updateInfo = new FileInfo();
            if (rootFileInfo != null) {
                String fileName = StringTools.rename(item.getFileName());
                updateInfo.setFileName(fileName);
            }
            updateInfo.setFilePid(filePid);
            this.fileInfoMapper.updateByFileIdAndUserId(updateInfo, item.getFileId(), userId);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeFile2RecycleBatch(String userId, String fileIds) {

        String[] fileIdArray = fileIds.split(",");

        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setFileIdArray(fileIdArray);
        fileInfoQuery.setUserId(userId);
        fileInfoQuery.setStatus(FileStatusEnums.USING.getStatus());
        List<FileInfo> fileInfoList = fileInfoService.findListByParam(fileInfoQuery);

        if (fileInfoList.isEmpty()) {
            return;
        }

        List<String> delFilePidList = new ArrayList<>();
        for (FileInfo fileInfo : fileInfoList) {
            findAllSubFolderFileIdList(delFilePidList, userId, fileInfo.getFileId(), FileDelFlagEnums.USING.getFlag());
        }

        //查看中间结果
        delFilePidList.forEach(fileId -> System.out.println("-------" + fileId + "-------------------------------------------------------"));

        //将目录下的所有文件更新为已删除
        if (!delFilePidList.isEmpty()) {
            FileInfo updateInfo = new FileInfo();
            updateInfo.setDelFlag(FileDelFlagEnums.DEL.getFlag());
            this.fileInfoMapper.updateFileDelFlagBatch(updateInfo, userId, delFilePidList, null, FileDelFlagEnums.USING.getFlag());
        }
        //将选中的文件更新为回收站
        List<String> delFileIdList = Arrays.asList(fileIdArray);
        FileInfo fileInfo = new FileInfo();
        fileInfo.setRecoveryTime(new Date());
        fileInfo.setDelFlag(FileDelFlagEnums.RECYCLE.getFlag());
        this.fileInfoMapper.updateFileDelFlagBatch(fileInfo, userId, null, delFileIdList, FileDelFlagEnums.USING.getFlag());
    }

    private void findAllSubFolderFileIdList(List<String> fileIdList, String userId, String fileId, Integer delFlag) {
        fileIdList.add(fileId);
        FileInfoQuery fileInfoQuery = new FileInfoQuery();

        fileInfoQuery.setUserId(userId);
        fileInfoQuery.setFilePid(fileId);
        fileInfoQuery.setDelFlag(delFlag);
        fileInfoQuery.setFolderType(FileFolderTypeEnums.FOLDER.getType());
        List<FileInfo> fileInfoList = fileInfoService.findListByParam(fileInfoQuery);
        for (FileInfo fileInfo : fileInfoList) {
            findAllSubFolderFileIdList(fileIdList, userId, fileInfo.getFileId(), delFlag);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class) //查询逻辑放在一起，修改和删除事务放在一起
    public void recoverFileBatch(String userId, String fileIds) {
        String[] fileIdArray = fileIds.split(",");
        FileInfoQuery query = new FileInfoQuery();
        query.setFileIdArray(fileIdArray);
        query.setDelFlag(FileDelFlagEnums.RECYCLE.getFlag());
        query.setUserId(userId);
        //查询出所有在回收站点的文件
        List<FileInfo> fileInfoList = this.fileInfoMapper.selectList(query);
        //将所有文件以及文件夹、以及文件夹中的子目录的id放入到
        List<String> delFileSubFolderFileIdList = new ArrayList<>();
        for (FileInfo fileInfo : fileInfoList) {
            if (FileFolderTypeEnums.FOLDER.getType().equals(fileInfo.getFolderType())) {
                findAllSubFolderFileIdList(delFileSubFolderFileIdList, userId, fileInfo.getFileId(), FileDelFlagEnums.DEL.getFlag());
            }
        }

        //查询所有根目录的文件
        query = new FileInfoQuery();
        query.setUserId(userId);
        query.setDelFlag(FileDelFlagEnums.USING.getFlag());
        query.setFilePid(Constants.ZERO_STR);
        List<FileInfo> allRootFileList = this.fileInfoMapper.selectList(query);
        Map<String, FileInfo> dbFileNameMap = allRootFileList.stream().collect(Collectors.toMap(FileInfo::getFileName, Function.identity(), (file1, file2) -> file2));

        //查询所有所选文件,将目录下的所有删除的文件更新为使用中
        if (!delFileSubFolderFileIdList.isEmpty()) {
            FileInfo fileInfo = new FileInfo();
            fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
            this.fileInfoMapper.updateFileDelFlagBatch(fileInfo, userId, delFileSubFolderFileIdList, null, FileDelFlagEnums.DEL.getFlag());
        }
        //将选择中的文件更新为正常，且父级目录到根目录
        List<String> delFileIdList = Arrays.asList(fileIdArray);
        FileInfo fileInfo = new FileInfo();
        fileInfo.setDelFlag(FileDelFlagEnums.USING.getFlag());
        fileInfo.setFilePid(Constants.ZERO_STR);
        fileInfo.setLastUpdateTime(new Date());
        this.fileInfoMapper.updateFileDelFlagBatch(fileInfo, userId, null, delFileIdList, FileDelFlagEnums.RECYCLE.getFlag());

        //将所有文件重命名
        for (FileInfo item : fileInfoList) {
            //使用map看是否原来root中的文件名和回收站的文件名有重复的
            FileInfo rootFileInfo = dbFileNameMap.get(item.getFileName());
            //文件名已经存在，重命名被还原的文件名
            if (rootFileInfo != null) {
                String fileName = StringTools.rename(item.getFileName());
                FileInfo updateInfo = new FileInfo();
                updateInfo.setFileName(fileName);
                this.fileInfoMapper.updateByFileIdAndUserId(updateInfo, item.getFileId(), userId);
            }
        }
    }

    @Override
    public void delFileBatch(String userId, String fileIds, Boolean adminOp) {

        // 将文件ID字符串分割为文件ID数组
        String[] fileIdArray = fileIds.split(",");

        // 创建FileInfoQuery对象并设置查询参数
        FileInfoQuery query = new FileInfoQuery();
        query.setUserId(userId); // 设置用户ID
        query.setFileIdArray(fileIdArray); // 设置文件ID数组
        query.setDelFlag(FileDelFlagEnums.RECYCLE.getFlag()); // 设置删除标志为回收站标志

        // 调用文件信息服务的方法，根据查询参数查找文件信息列表
        List<FileInfo> fileInfoList = fileInfoService.findListByParam(query);

        //如果是文件夹删除子文件
        List<String> delFileSubFileFolderIdList = new ArrayList<>();
        for (FileInfo fileInfo : fileInfoList) {
            if (FileFolderTypeEnums.FOLDER.getType().equals(fileInfo.getFolderType())) {
                findAllSubFolderFileIdList(delFileSubFileFolderIdList, userId, fileInfo.getFileId(), FileDelFlagEnums.DEL.getFlag());
            }
        }
        //删除所选文件子目录中的文件
        if (!delFileSubFileFolderIdList.isEmpty()) {
            this.fileInfoMapper.delFileBatch(userId, delFileSubFileFolderIdList, null, adminOp ? null : FileDelFlagEnums.DEL.getFlag());
        }
        //删除所选文件
        this.fileInfoMapper.delFileBatch(userId,null,Arrays.asList(fileIdArray) , adminOp ? null : FileDelFlagEnums.RECYCLE.getFlag());

        Long useSpace = fileInfoService.getUserUseSpace(userId);
        UserInfo userInfo = new UserInfo();
        userInfo.setUseSpace(useSpace);
        this.userInfoMapper.updateByUserId(userInfo, userId);
        //设置缓存
        UserSpaceDto userSpaceDto = redisComponent.getUserSpaceUse(userId);
        userSpaceDto.setUseSpace(useSpace);
        redisComponent.saveUserSpaceUse(userId, userSpaceDto);
    }

    /**
     * 检查根文件的父ID是否合法
     *
     * 此方法主要用于验证给定的根文件父ID（rootFilePid）与文件ID（fileId）之间的关系，
     * 以及它们是否符合预期的层级结构关系如果给定的文件ID为空，则抛出异常，
     * 表示缺少必要的参数如果根文件的父ID与文件ID相同，则认为验证通过，
     * 因为这通常表示操作针对的是根文件本身如果两者不相同，则进一步调用checkFilePid方法进行详细检查
     *
     * @param rootFilePid 根文件的父ID，用于验证文件的层级关系
     * @param userId 用户ID，可能用于权限验证或日志记录
     * @param fileId 文件ID，需要验证的文件的ID
     * @throws BusinessException 当文件ID为空时抛出，表示参数不合法
     */
    @Override
    public void checkRootFilePid(String rootFilePid, String userId, String fileId) {
        // 检查文件ID是否为空，如果为空则抛出异常
        if (StringTools.isEmpty(fileId)) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }
        // 如果根文件的父ID与文件ID相同，则直接返回，无需进一步检查
        if (rootFilePid.equals(fileId)) {
            return;
        }
        // 调用方法进一步检查文件的父ID是否合法
        checkFilePid(rootFilePid, fileId, userId);
    }

    /**
     * 递归检查文件的父ID是否与指定的根文件ID相匹配
     * 此方法用于验证文件的层级关系，确保操作的文件属于正确的目录结构
     *
     * @param rootFilePid 根文件的PID，用于比较
     * @param fileId 当前要检查的文件ID
     * @param userId 用户ID，用于验证文件归属
     * @throws BusinessException 如果文件信息不存在或文件PID是"0"，抛出业务异常
     */
    private void checkFilePid(String rootFilePid, String fileId, String userId) {
        // 根据文件ID和用户ID查询文件信息
        FileInfo fileInfo = this.fileInfoMapper.selectByFileIdAndUserId(fileId, userId);
        // 如果文件信息不存在，抛出业务异常
        if (fileInfo == null) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }
        // 如果文件的父ID是"0"，表示没有父目录，抛出业务异常
        if (Constants.ZERO_STR.equals(fileInfo.getFilePid())) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }
        // 如果文件的父ID与根文件PID匹配，表示找到了对应的根文件，结束递归
        if (fileInfo.getFilePid().equals(rootFilePid)) {
            return;
        }
        // 递归调用，继续检查当前文件的父文件
        checkFilePid(rootFilePid, fileInfo.getFilePid(), userId);
    }

    /**
     * 检查文件名是否唯一
     * 在特定的文件夹类型和父文件夹下，确保文件名是唯一的，避免文件冲突
     *
     * @param filePid    父文件夹的ID，用于定位文件的上级目录
     * @param userId     用户ID，用于确定文件的所有者
     * @param fileName   待检查的文件名
     * @param folderType 文件夹类型，用于区分不同的文件存储区域
     * @throws BusinessException 如果存在同名文件，则抛出业务异常，提示修改文件名
     */
    private void checkFileName(String filePid, String userId, String fileName, Integer folderType) {
        // 初始化FileInfoQuery对象，设置查询条件
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setFolderType(folderType);
        fileInfoQuery.setFileName(fileName);
        fileInfoQuery.setFilePid(filePid);
        fileInfoQuery.setUserId(userId);
        //fileInfoQuery.setDelFlag(FileDelFlagEnums.USING.getFlag());
        fileInfoQuery.setDelFlag(FileDelFlagEnums.USING.getFlag());

        // 查询符合条件的文件数量
        Integer count = this.fileInfoMapper.selectCount(fileInfoQuery);

        // 如果存在同名文件，则抛出异常
        if (count > 0) {
            throw new BusinessException("此目录下已存在同名文件，请修改名称");
        }
    }

    /**
     * 更新用户的空间使用情况
     *
     * @param webUserDto 用户信息DTO，包含用户ID等信息
     * @param useSpace   要更新的使用空间大小
     *                   <p>
     *                   此方法首先通过用户ID和要更新的使用空间大小，调用userInfoMapper的updateUserSpace方法来更新数据库中的用户空间使用信息
     *                   如果更新操作没有影响到任何行（即返回的count为0），则抛出一个BusinessException，表示更新操作失败
     *                   然后，从Redis中获取用户的空间使用信息，更新其使用空间大小，并将新的信息存回Redis
     */
    private void updateUserSpace(SessionWebUserDto webUserDto, Long useSpace) {
        // 更新数据库中的用户空间使用信息
        Integer count = userInfoMapper.updateUserSpace(webUserDto.getUserId(), useSpace, null);
        // 如果数据库更新操作失败，则抛出异常
        if (count == 0) {
            throw new BusinessException(ResponseCodeEnum.CODE_904);
        }
        // 从Redis中获取用户的空间使用信息
        UserSpaceDto spaceDto = redisComponent.getUserSpaceUse(webUserDto.getUserId());
        // 更新用户的使用空间大小
        spaceDto.setUseSpace(spaceDto.getUseSpace() + useSpace);
        // 将更新后的用户空间使用信息存回Redis
        redisComponent.saveUserSpaceUse(webUserDto.getUserId(), spaceDto);
    }

    @Override
    public Long getUserUseSpace(String userId) {
        return this.fileInfoMapper.selectUseSpace(userId);
    }

    @Override
    @Transactional
    public void saveShare(String shareRootFilePid, String shareFileIds, String myFolderId, String shareUserId, String cureentUserId) {
        //获取目标文件ID数组
        String[] shareFileIdArray = shareFileIds.split(",");
        //目标目录文件列表，设置查询条件
        FileInfoQuery fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setUserId(cureentUserId);
        //要放置到目标文件夹的位置
        fileInfoQuery.setFilePid(myFolderId);
        //查询当前目录下的所有文件
        List<FileInfo> currentFileList = this.fileInfoMapper.selectList(fileInfoQuery);

        //将currentFileList文件列表转为Map，key为文件名，value为文件对象；
        Map<String, FileInfo> currentFileMap = currentFileList.stream().collect(Collectors.toMap(FileInfo::getFileName, Function.identity(), (file1, file2) -> file2));

        //设置查询条件，选择的文件，放入到shareFileList中
        fileInfoQuery = new FileInfoQuery();
        fileInfoQuery.setUserId(shareUserId);
        fileInfoQuery.setFileIdArray(shareFileIdArray);
        List<FileInfo> shareFileList = this.fileInfoMapper.selectList(fileInfoQuery);

        //重命名选择的文件
        List<FileInfo> copyFileList = new ArrayList<>();
        // 获取当前日期，用于后续可能的时间设置或比较
        Date curDate = new Date();
        // 遍历共享文件列表，以处理每个文件的命名问题
        for (FileInfo item : shareFileList) {
            // 检查当前文件是否在已有的文件映射中存在
            FileInfo haveFile = currentFileMap.get(item.getFileName());
            // 如果当前文件与已有文件重名，则重命名当前文件以避免冲
            if (haveFile != null) {
                item.setFileName(StringTools.rename(item.getFileName()));
            }
            findAllSubFile(copyFileList, item, shareUserId, cureentUserId, curDate, myFolderId);
        }
        this.fileInfoMapper.insertBatch(copyFileList);

        //更新空间
        Long useSpace = this.fileInfoMapper.selectUseSpace(cureentUserId);
        UserInfo dbUserInfo = this.userInfoMapper.selectByUserId(cureentUserId);
        if (useSpace > dbUserInfo.getTotalSpace()) {
            throw new BusinessException(ResponseCodeEnum.CODE_904);
        }
        UserInfo userInfo = new UserInfo();
        userInfo.setUseSpace(useSpace);
        this.userInfoMapper.updateByUserId(userInfo, cureentUserId);
        //设置缓存
        UserSpaceDto userSpaceDto = redisComponent.getUserSpaceUse(cureentUserId);
        userSpaceDto.setUseSpace(useSpace);
        redisComponent.saveUserSpaceUse(cureentUserId, userSpaceDto);
    }

    /**
     * 递归查找并复制所有子文件
     *
     * 该方法用于复制指定文件及其所有子文件（如果指定文件是文件夹），将复制的文件列表添加到目标列表中
     * 它会更新文件的创建时间、最后更新时间、父文件ID和用户ID，同时为新文件生成随机ID
     *
     * @param copyFileList 存储复制后的文件信息列表
     * @param fileInfo 当前正在处理的文件信息对象
     * @param sourceUserId 原始文件的用户ID
     * @param currentUserId 当前操作的用户ID，用于设置复制后文件的用户ID
     * @param curDate 当前日期，用于设置复制后文件的创建时间和最后更新时间
     * @param newFilePid 新文件的父文件ID，用于设置复制后文件的父文件ID
     */
    private void findAllSubFile(List<FileInfo> copyFileList, FileInfo fileInfo, String sourceUserId, String currentUserId, Date curDate, String newFilePid) {
        // 获取当前文件的原始ID
        String sourceFileId = fileInfo.getFileId();

        // 更新文件的创建时间和最后更新时间
        fileInfo.setCreateTime(curDate);
        fileInfo.setLastUpdateTime(curDate);

        // 设置文件的父文件ID和用户ID
        fileInfo.setFilePid(newFilePid);
        fileInfo.setUserId(currentUserId);

        // 为新文件生成随机ID
        String newFileId = StringTools.getRandomString(Constants.LENGTH_10);
        fileInfo.setFileId(newFileId);

        // 将更新后的文件信息添加到复制文件列表中
        copyFileList.add(fileInfo);

        // 如果当前文件是文件夹，则递归查找并复制其所有子文件
        if (FileFolderTypeEnums.FOLDER.getType().equals(fileInfo.getFolderType())) {
            FileInfoQuery query = new FileInfoQuery();
            //文件父级id被更新，因为当前文件是文件夹，所以需要更新
            query.setFilePid(sourceFileId);
            query.setUserId(sourceUserId);

            // 查询原始文件夹下的所有子文件
            List<FileInfo> sourceFileList = this.fileInfoMapper.selectList(query);

            // 遍历子文件列表，递归复制每个子文件
            for (FileInfo item : sourceFileList) {
                findAllSubFile(copyFileList, item, sourceUserId, currentUserId, curDate, newFileId);
            }
        }
    }
}